from copy import deepcopy
from threading import Thread
import time
import os
import os.path

from ma.commons.core.abstractmapper import AbstractMapper
from ma.commons.core.constants import *
from ma.commons.core.taskinprogress import TaskInProgress
from ma.commons.core.maptaskrunner import MapTaskRunner
from ma.commons.core.maptipinfo import MapTIPInfo
from ma.commons.core.processrunner import ProcessRunner
from .partitioner import Partitioner
from ma.utils import location
import ma.const
import ma.log


class MapTIP(TaskInProgress, Thread):    
    """This class derives from TaskInProgress and keeps functionality that is 
    relevant to a map task only
    """
    
    def __init__(self, nodetracker, maptip_info=None, struct_id=STRUCT_ID_DEFAULT):
        """arguments description:
        nodetracker is the ref of tracker this task will run on
        maptip_info is a MapTIPInfo kind of pre fabricated object sent to init
        """
        
        # get the logger
        self.__log = ma.log.get_logger("ma.mapred")
                     
        TaskInProgress.__init__(self, nodetracker)
        Thread.__init__(self)
        
        #this is the dict to which the user function will emit its key, value tuples
        #the struct is key -> list_of_values
        #this is the key value dict that will persist for the life of this mapTIP as
        #files are read one by one by the map and the key value pairs emitted will be
        #appended to it and then finally written to a file
        self.key_value_dict = {}
        
        #to overshadow inherited TaskInProgressInfo object with a MapTIPInfo object
        if maptip_info is None:
            # a mapTIPinfo with default setting with no affiliation to a job or task
            self.tip_info = MapTIPInfo(-1,-1)
        else:
            self.tip_info = deepcopy(maptip_info)
          
        self.threads = []
        
        user_process_id = self.tip_info.returnTaskID()
        
        #the task runner object that will launch the user provided by TaskRunner
        user_map_module = ma.const.JobsXmlData.get_str_data(ma.const.xml_map_module_name, self.tip_info.job_id)
        user_map_class = ma.const.JobsXmlData.get_str_data(ma.const.xml_map_class_name, self.tip_info.job_id)
        print(user_map_class, user_map_module)
        user_runner_program = location.get_src_ma_path() + ma.const.JobsXmlData.get_filepath_str_data(ma.const.xml_mr_maptask_runner, self.tip_info.job_id)
        
        # get the filename which holds the reduce compute misc. output
        proc_temp_filename = ma.const.JobsXmlData.get_str_data(ma.const.xml_map_proc_temp_filename, self.tip_info.job_id, self.tip_info.task_id)
        ProcessRunner.store_pickled_data([self.tip_info], proc_temp_filename, self.tip_info.job_id)
        self.process_runner = ProcessRunner(user_runner_program, [user_map_module, user_map_class, self.tip_info.job_id, self.tip_info.task_id], user_process_id)
        
        # self.mapper = AbstractMapper(self.key_value_dict)
        
        self.partitioner = Partitioner(self.tip_info.job_id)
        
        self.struct_id = struct_id
        
                    
    def localizeTask(self):
        """This function copies the input split for the map to work on from the
        HDFS to the local file system. Also it deletes any map output files from a 
        previous attempt of this map on this node
        """  
        
        copied_inputs = []
        
        self.input_dest_dir = ma.const.JobsXmlData.get_filepath_str_data(ma.const.xml_local_input_temp_dir, self.tip_info.job_id)
        
        # localize input one by one
        for file in self.tip_info.input_data:
            # TODO: This input is coming from the HDFS interactor ... it could have multiple inputs
            dfs_input_filepath = ma.const.JobsXmlData.get_dfs_filepath_str_data(ma.const.xml_dfs_path_job_input, self.tip_info.job_id) + ma.const.dfs_dir_sep + file[0]
            
            self.__log.debug('Copying map input for %s: %s', self.tip_info.returnTaskID(), str(dfs_input_filepath))
            
            input_dest_filepath = os.path.join(self.input_dest_dir, os.path.basename(dfs_input_filepath))
            if os.path.isfile(input_dest_filepath):
                self.__log.info('Deleting old local file: %s', input_dest_filepath)
                os.remove(input_dest_filepath)

            # TODO: Offset dealing while copying
            ret = self.nodetracker.hdfs.copy_file_to_local(dfs_input_filepath, self.input_dest_dir, self.tip_info.job_id, auto_retry=True)
            
            copied_inputs.append(file[0])
            
            #if transfer wasn't successful
            if not ret:
                self.__log.error("Localize Task Failed for map-task %d", self.tip_info.task_id)
                return False
              
        self.__log.info('Copied all map inputs for %s: %s', self.tip_info.returnTaskID(), str(copied_inputs))
        
        return True
    
    
    def launchTask(self):    
        """this function will launch the ProcessRunner. It returns true on
        success 
        """ 
        
        arglist = [self.tip_info.job_id, self.tip_info.task_id, MAP]
        self.pingtimer_id = self.nodetracker.timer.addTimer(self.task_ping_interval, self.nodetracker.healthPingFromTask, arglist)
        
        # note the starting time
        self.tip_info.start_time = time.time()
        
        # start the process ... and wait till it ends
        ret = self.process_runner.start_and_end_process()
        
        # note the end time
        self.tip_info.end_time = time.time()
        
        return ret
    
    
    def taskComplete(self):    
        """this function will indicate that the user map func has ended; in response
        this func must broadcast to the other NTs 
        that a certain map is complete and its output is up for grabs! 
        """
        
        # do the cleanup, delete the input files
        for file in self.tip_info.input_data:
            input_filepath = os.path.join(self.input_dest_dir, file[0])
            os.remove(input_filepath)
         
        self.__log.info('Map task %s completed processing', self.tip_info.returnTaskID())
        
        self.nodetracker.taskComplete(self.tip_info) 
        
                
    def run(self):
        """This function will run as this thread is started and marks the flow of 
        how this MapTIP will function
        """    
        
        self.__log.info('Started MapTIP for %s', self.tip_info.returnTaskID())
        
        # if localize didn't happen correctly
        if not self.localizeTask():
            self.nodetracker.killTask(self.tip_info.job_id, MAP, self.tip_info.task_id)
            self.__log.error('Couldn\'t localize task for Map %s', self.tip_info.returnTaskID())
            raise IOError('The MapTIP couldn\'t shuffle data for Map %d' % self.tip_info.task_id)
        
        #launch and finish task
        if self.launchTask():   
            #marks task as complete
            self.taskComplete()
        else:
            # TODO: Called failed task
            self.__log.error('Map task %s failed while processing', self.tip_info.returnTaskID())
        
        self.nodetracker.timer.removeTimer(self.pingtimer_id)
        
        #running in a separate process and if the process dies before completion 
        #it shud just remove the ping timer so that the health pings stop and 
        #the node tracker registers that the task is dead!
        #subprocess Popen object has a poll method!
        
        #send input to user func in a loop line by line ...is that ok!?

